import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)
import java.util.*;
import java.lang.reflect.*;

/**
 * This class represents StackMan.
 * <br />
 * It is a Singleton.
 * 
 * @author (Thomas KOBER)
 * @author (Dilyan GEORGIEV)
 * 
 * @version (1.0)
 */
public class StackMan extends Actor
{
    public  static final    int         INIT_LIVES      = 3;
    public  static final    int         INIT_SCORE      = 0;
    public  static final    int         BOUND_DISTANCE  = 12;
    private static final    int         IMPACT_DISTANCE = 5;
    private static          StackMan    instance        = null;
    
    // GodMode on/off switch
    private boolean                         godMode;
    // HeartBeat used for biting behaviour
    private int                             heartBeat;
    // Lives Count
    private int                             lives;
    // Score Count
    private int                             score;
    // Magic Backpack
    private MagicBackpack<SpecialItem>      backpack;
    // StackMans normal skin
    private String                          imgNormal;
    // StackMans left skin
    private String                          imgLeft;
    // Collection of CollisionObservers
    private ArrayList<CollisionObserver>    observers;
    
    /**
     * Constructor for objects of class StackMan.
     * Private because of Singleton implementation.
     */
    private StackMan()
    {
        this.heartBeat  = 0;
        
        this.lives      = INIT_LIVES;
        this.score      = INIT_SCORE;
        
        this.godMode    = false;
        
        // Start with the Backpack being a Queue
        this.backpack   = new QueueBackpack<SpecialItem>();
        
        this.observers  = new ArrayList<CollisionObserver>();
        
        this.imgNormal  = "StackMan_Open.png";
        this.imgLeft    = "StackMan_Open_Left.png";
    }
    
    /**
     * Returns the StackMan instance.
     * @return the StackMan instance.
     */
    public static StackMan getInstance()
    {
        if (instance == null) {
            instance = new StackMan();
        }
        
        return instance;
    }
    
    /**
     * Act - do whatever the StackMan wants to do. This method is called whenever
     * the 'Act' or 'Run' button gets pressed in the environment.
     */
    public void act() 
    {
        if (!((StackWorld)super.getWorld()).isPaused()) {
            
            if (Greenfoot.isKeyDown("6") && Greenfoot.isKeyDown("escape")) {
                this.godMode = !this.godMode;
            }
            
            this.checkCollision();
            
            this.selectImage();
            
            if (super.getRotation() == 180) {
                super.setImage(this.imgLeft);
            } else {
                super.setImage(this.imgNormal);
            }
            
            this.move();

            if (Greenfoot.isKeyDown("right") && !disableRight()) {
                super.setImage(this.imgNormal);
                super.setRotation(0);
            } else if (Greenfoot.isKeyDown("down") && !disableDown()) {
                super.setImage(this.imgNormal);
                super.setRotation(90);
            } else if (Greenfoot.isKeyDown("left") && !disableLeft()) {
                super.setImage(this.imgLeft);
                super.setRotation(180);
            } else if (Greenfoot.isKeyDown("up") && !disableUp()) {
                super.setImage(this.imgNormal);
                super.setRotation(270);
            }
        }
    }   
    
    /**
     * Move with respect to GodMode and Labyrinth constraints.
     */
    private void move() {
        
        if (!this.godMode) {
            int speed = impactAhead() ? 0:1;
            move(speed);
        } else {
            super.move(1);
        }
    }
    
    /**
     * Checks if an obstacle is ahead.
     * @return true if there is an obstacle ahead, false otherwise.
     */
    private boolean impactAhead() {
        
        boolean hit = false;
        
        if (!this.godMode) {
        
            int origX = super.getX();
            int origY = super.getY();
            int dir = super.getRotation();
            if(dir == 0 ) {
                setLocation(super.getX() + IMPACT_DISTANCE, super.getY());
            } else if (dir == 90 ) {
                setLocation(super.getX(), super.getY() + IMPACT_DISTANCE);
            } else if (dir == 180) {
                setLocation(super.getX() - IMPACT_DISTANCE , super.getY());
            } else if (dir == 270) {
                setLocation(super.getX(), super.getY() - IMPACT_DISTANCE);
            }
            Actor actor;
            actor = getOneIntersectingObject(Obstacle.class);
            if(actor != null) {
                hit = true;
            }
            setLocation(origX, origY);
        }
        return hit;        
    }

    /**
     * Checks if the possibility to move left needs to be disabled.
     * @return true if it needs to be disabled, false otherwise.
     */
    private boolean disableLeft() {
        boolean item = false; 
        
        if (!this.godMode) {
        
            int origX = super.getX();
            int origY = super.getY();
            int x = super.getX() - BOUND_DISTANCE;
            setLocation(x, getY());
            Actor t = getOneIntersectingObject(Obstacle.class);
            Obstacle k = (Obstacle)t;
            if(t != null) {
                item = true;
            }
            setLocation(origX,origY);
        }
        return item;        
    }

    /**
     * Checks if the possibility to move right needs to be disabled.
     * @return true if it needs to be disabled, false otherwise.
     */
    private boolean disableRight() {

        boolean item = false; 
        
        if (!this.godMode) {
            int origX = super.getX();
            int origY = super.getY();
            int x = super.getX() + BOUND_DISTANCE;
            setLocation(x, getY());
            Actor t = getOneIntersectingObject(Obstacle.class);
            Obstacle k = (Obstacle)t;
            if(t != null) {
                item = true;
            }
            setLocation(origX,origY);
        }
        
        return item;

    }

    /**
     * Checks if the possibility to move up needs to be disabled.
     * @return true if it needs to be disabled, false otherwise.
     */
    private boolean disableUp() {
        boolean item = false; 
        
        if (!this.godMode) {
        
            int origX = super.getX();
            int origY = super.getY();
            int y = super.getY() - BOUND_DISTANCE;
            setLocation(getX(), y);
            Actor t = getOneIntersectingObject(Obstacle.class);
            Obstacle k = (Obstacle)t;
            if(t != null) {
                item = true;
            }
            setLocation(origX,origY);
        }
        
        return item;
    }

    /**
     * Checks if the possibility to move down needs to be disabled.
     * @return true if it needs to be disabled, false otherwise.
     */
    private boolean disableDown() {
        boolean item = false; 
        
        if (!this.godMode) {
        
            int origX = super.getX();
            int origY = super.getY();
            int y = super.getY() + BOUND_DISTANCE;
            setLocation(getX(), y);
            Actor t = getOneIntersectingObject(Obstacle.class);
            Obstacle k = (Obstacle)t;
            if(t != null) {
                item = true;
            }
            setLocation(origX,origY);
        }
        
        return item;
    }
    
    /**
     * Returns the current number of lives.
     * @return the current number of lives.
     */
    public int getLives()
    {
        return this.lives;
    }
    
    /**
     * Initialises the starting number of lives.
     */
    public void initLives()
    {
        this.lives = INIT_LIVES;
    }
    
    /**
     * Initalises the starting score.
     */
    public void initScore()
    {
        this.score = INIT_SCORE;
    }
    
    /**
     * Returns the current score.
     * @return the current score.
     */
    public int getScore()
    {
        return this.score;
    }
    
    /**
     * Increments the score by the given value.
     * @param score the value by which the current score is incremented.
     */
    public void incrementScore(int score)
    {
        this.score += score;
    }
    
    /**
     * Decrements the number of lives by 1.
     */
    public void decrementLives()
    {
        this.lives--;
    }
    
    /**
     * Returns the String representation of the current backpack type.
     * @return the String representation of the current backpack type.
     */
    public String getCurrentBackpackType()
    {
        return this.backpack.getDisplayType();
    }
    
    /**
     * Returns the magic backpack.
     * @return the magic backpack.
     */
    public MagicBackpack getBackpack()
    {
        return this.backpack;
    }
 
    /**
     * (Re)sets the magic backpack.
     * @param backpack the new magic backpack.
     */
    public void setBackpack(MagicBackpack backpack)
    {
        this.backpack = backpack;
    }
    
    /**
     * Select image for StackMan (for biting) with respect to the current heartBeat.
     */
    private void selectImage()
    {
        // Select StackMan img based on heartBeat
        if (this.heartBeat >= 20 && this.heartBeat <= 40) {
            this.imgNormal = "StackMan_Closed.png";
            this.imgLeft = "StackMan_Closed_Left.png";
        } else if (this.heartBeat <= 20) {
            this.imgNormal = "StackMan_Open.png";
            this.imgLeft = "StackMan_Open_Left.png";
        } else if (this.heartBeat > 40) {
            this.heartBeat = 0;
        }
        
        // Hearbeat is used for the change of the StackMan img (Open mouth vs. Closed mouth)
        this.heartBeat++;
    }
    
    /**
     * Kill ghosts.
     */
    private void killGhosts()
    {
        //It will kill two ghosts.
        World w = getWorld();
        List<Ghost> g = w.getObjects(Ghost.class);
        if(g != null) {
            while(g.size() != 1) {
                int removeGhost = Greenfoot.getRandomNumber(g.size());
                w.removeObject(g.get(removeGhost));
                g = w.getObjects(Ghost.class);
            }
        }
    }
    
    /**
     * Check if there is a collision at the current position.
     */
    private void checkCollision()
    {
        
        Class<CollisionObserver> clazz = CollisionObserver.class;
        Class[] methodParameters = null;
        Object[] formalParameters = null;
        Method collision = null;
        
        if (super.getOneObjectAtOffset(0, 0, Ghost.class) != null) {
            try {
                collision = clazz.getDeclaredMethod("handleStackManGhostCollision", methodParameters);
            } catch (NoSuchMethodException ex) {
                ex.printStackTrace();
            } catch (SecurityException ex) {
                ex.printStackTrace();
            }
        } else if (super.getOneObjectAtOffset(0, 0, SpecialItem.class) != null) {
            
            // Bag item
            SpecialItem theItem = (SpecialItem)super.getOneObjectAtOffset(0, 0, SpecialItem.class);
            
            if (theItem.getItemValue() == UltimateItem.ITEM_VALUE) {
                this.killGhosts();
            } else {
                this.backpack.put(theItem);
                this.score += 10; 
            }
           
            // Notify Observers
            try {
                methodParameters = new Class[1];
                methodParameters[0] = SpecialItem.class;
                
                formalParameters = new Object[1];
                formalParameters[0] = theItem;
                
                collision = clazz.getDeclaredMethod("handleStackManItemCollision", methodParameters);
            } catch (NoSuchMethodException ex) {
                ex.printStackTrace();
            } catch (SecurityException ex) {
                ex.printStackTrace();
            }
        } else if (super.getOneObjectAtOffset(0, 0, HomeBase.class) != null) {
            try {
                collision = clazz.getDeclaredMethod("handleStackManHomeBaseCollision", methodParameters);
            } catch (NoSuchMethodException ex) {
                ex.printStackTrace();
            } catch (SecurityException ex) {
                ex.printStackTrace();
            }
        }
        
        if (collision != null) {
            this.sendStackManCollisionNotification(collision, formalParameters);
        }
    }
    
    /**
     * Return one intersecting object of the given class.
     * @param cls class of object to be checked for intersection.
     * @return intersecting object.
     */
    protected Actor getOneIntersectingObject(Class cls)
    {
        return super.getOneIntersectingObject(cls);
    }
    
    // CollisionObserver Code
    
    /**
     * Register a new CollisionObserver.
     * @param theObserver a new CollisionObserver.
     */
    public void registerCollisionObserver(CollisionObserver theObserver)
    {
        this.observers.add(theObserver);
    }
    
    /**
     * Unregister an existing CollisionObserver.
     * @param theObserver the observer to be unregistered.
     */
    public void removeCollisionObserver(CollisionObserver theObserver)
    {
        this.observers.remove(theObserver);
    }
    
    /**
     * Sends a CollisionNotification to all of its observers.
     */
    public void sendStackManCollisionNotification(Method theMethod, Object[] formalParameters)
    {
        for (CollisionObserver o : this.observers) {
            try {
                theMethod.invoke(o, formalParameters); // Using Reflection here because I'm lazy...
            } catch (IllegalAccessException ex) {
                ex.printStackTrace();
            } catch (InvocationTargetException ex) {
                ex.printStackTrace();
            }
        }
    }
    
    /**
     * Returns the objects of the given class in the given range.
     * @param radius the radius to check.
     * @param cls the class of the object which are checked.
     * @return list of objects in range.
     */
    protected List getObjectsInRange(int radius, Class cls)
    {
        return super.getObjectsInRange(radius, cls);
    }
    
}
